﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;

namespace NetFrameworkExtensions.Enterprise.Bus
{
  public class ChangeSubscriptionsImpl : IChangeSubscriptions
  {
    private object syncLock = new object();
    private List<EventHandler<EntityChangedEventArgs>> globalSubscribers = new List<EventHandler<EntityChangedEventArgs>>();
    private Dictionary<Type, IList> subscribers = new Dictionary<Type, IList>();
    private List<Delegate> currentPublishers;
    private int currentPublishingCounter;

    public void AddSubscription(EventHandler<EntityChangedEventArgs> eventHandler)
    {
      if (eventHandler == null)
        throw new ArgumentNullException("eventHandler");
      lock (syncLock)
      {
        globalSubscribers.Add(eventHandler);
      }
    }

    public void RemoveSubscription(EventHandler<EntityChangedEventArgs> eventHandler)
    {
      if (eventHandler == null)
        throw new ArgumentNullException("eventHandler");
      lock (syncLock)
      {
        var pos = globalSubscribers.IndexOf(eventHandler);
        if (pos >= 0)
          globalSubscribers.RemoveAt(pos);
      }
    }

    public void AddSubscription<TEntity>(EventHandler<EntityChangedEventArgs<TEntity>> eventHandler) where TEntity : class
    {
      if (eventHandler == null)
        throw new ArgumentNullException("eventHandler");
      lock (syncLock)
      {
        IList list;
        var type = typeof(TEntity);
        if (!subscribers.TryGetValue(type, out list))
        {
          list = new List<EventHandler<EntityChangedEventArgs<TEntity>>>();
          subscribers[type] = list;
        }
        list.Add(eventHandler);
      }
    }

    public void RemoveSubscription<TEntity>(EventHandler<EntityChangedEventArgs<TEntity>> eventHandler) where TEntity : class
    {
      if (eventHandler == null)
        throw new ArgumentNullException("eventHandler");
      lock (syncLock)
      {
        IList list;
        var type = typeof(TEntity);
        if (subscribers.TryGetValue(type, out list))
        {
          var pos = list.IndexOf(eventHandler);
          if (pos >= 0)
          {
            list.RemoveAt(pos);
            if (list.Count == 0)
              subscribers.Remove(type);
          }
        }
      }
    }

    public void PublishChange(object sender, object entity, EntityChangeOperation operation)
    {
      if (sender == null) throw new ArgumentNullException("sender");
      if (entity == null) throw new ArgumentNullException("entity");
      if (!Enum.IsDefined(typeof(EntityChangeOperation), operation)) throw new ArgumentOutOfRangeException("operation");
      PublishChangeInternal(sender, entity, entity.GetType(), operation);
    }

    public void PublishChange(object sender, object entity, Type entityType, EntityChangeOperation operation)
    {
      if (sender == null) throw new ArgumentNullException("sender");
      if (entity == null) throw new ArgumentNullException("entity");
      if (entityType == null) throw new ArgumentNullException("entityType");
      if (!Enum.IsDefined(typeof(EntityChangeOperation), operation)) throw new ArgumentOutOfRangeException("operation");
      PublishChangeInternal(sender, entity, entityType, operation);
    }

    public void PublishChange<TEntity>(object sender, TEntity entity, EntityChangeOperation operation) where TEntity : class
    {
      if (sender == null) throw new ArgumentNullException("sender");
      if (entity == null) throw new ArgumentNullException("entity");
      if (!Enum.IsDefined(typeof(EntityChangeOperation), operation)) throw new ArgumentOutOfRangeException("operation");
      PublishChangeInternal(sender, entity, typeof(TEntity), operation);
    }

    private void PublishChangeInternal(object sender, object entity, Type entityType, EntityChangeOperation operation)
    {
      IList<EventHandler<EntityChangedEventArgs>> globals = null;
      IList<Delegate> typeSpecific = null;
      lock (syncLock)
      {
        if (currentPublishers == null)
          currentPublishers = new List<Delegate>();

        globals = globalSubscribers.ToList();

        IList list;
        if (subscribers.TryGetValue(entityType, out list))
          typeSpecific = list.Cast<Delegate>().ToList();
        currentPublishingCounter++;
      }

      try
      {
        var untypedArgs = new EntityChangedEventArgs(entity, operation);
        foreach (var handler in globals)
        {
          bool call = true;
          lock (syncLock)
          {
            if (currentPublishers.Contains(handler))
              call = false;
            else
              currentPublishers.Add(handler);
          }
          if (!call) continue;
          try
          {
            handler(sender, untypedArgs);
          }
          catch (Exception ex)
          {
            throw;
          }
        }

        if (typeSpecific != null)
        {
          var typedArgs = Activator.CreateInstance(typeof(EntityChangedEventArgs<>).MakeGenericType(entityType), entity, operation);
          foreach (var handler in typeSpecific)
          {
            bool call = true;
            lock (syncLock)
            {
              if (currentPublishers.Contains(handler))
                call = false;
              else
                currentPublishers.Add(handler);
            }
            if (!call) continue;
            try
            {
              handler.DynamicInvoke(sender, typedArgs);
            }
            catch (Exception ex)
            {
              throw;
            }
          }
        }
      }
      finally
      {
        lock (syncLock)
        {
          currentPublishingCounter--;

          if (currentPublishingCounter == 0)
            currentPublishers = null;
        }
      }
    }
  }
}
